package fr.n7.sma.model;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import javax.swing.event.EventListenerList;

import fr.n7.sma.model.listeners.AgentListener;
import fr.n7.sma.model.listeners.CellListener;
import fr.n7.sma.model.listeners.GridModelListener;
import fr.n7.sma.model.listeners.events.AgentEvent;
import fr.n7.sma.model.listeners.events.CellEvent;
import fr.n7.sma.model.listeners.events.GridModelEvent;
import fr.n7.sma.model.listeners.events.GridModelEvent.EventType;

/**
 * It implements a grid, composed of cell. Needed infos (such as width, height,
 *  cell...) are accessible through this class. Methods to add attractor,
 *  compute next position or probas are also provided in this class.
 * 

 * @author bbaccot && clej37
 *
 */
public class GridModel<CellType extends ICell<CellProp>, AgentProperty, CellProp>
		implements CellListener<CellProp>,
		AgentListener<AgentProperty, CellProp> {

	private EventListenerList listeners;

	private CellType[][] grid;

	private Collection<Attractor<CellProp>> attractors;

	private List<Agent<AgentProperty, CellProp>> agents;

	private float pheroForce = 5;
	private float pheroRadius = 2;
	
	public int width;

	public int height;

	private float evapRate;

	public GridModel(
			CellType[][] cs, 
			float evapRate,
			int vision,
			double pheroForce,
			float pheroRadius) throws IllegalArgumentException {
		this.grid = cs;
		this.evapRate = evapRate;
		setVision(vision);
		setPheroForce(pheroForce);
		setPheroRadius(pheroRadius);
		this.attractors = new ArrayList<Attractor<CellProp>>();
		this.listeners = new EventListenerList();
		this.agents = Collections
				.synchronizedList(new ArrayList<Agent<AgentProperty, CellProp>>());

		width = grid.length;
		height = grid[0].length;
		if (width <= 0 || height <= 0) {
			throw new IllegalArgumentException(
					"Width and height must be strictly positive.");
		}

		for (short x = 0; x < width; x++) {
			for (short y = 0; y < height; y++) {
				CellType c = cs[x][y];
				c.addCellListener(this);
				List<short[]> l = getNeighbours(new short[] { x, y });
				for (short[] neighbour : l) {
					c.addCellListener(cs[neighbour[0]][neighbour[1]]);
				}
			}
		}
	}

	public void addAgent(Agent<AgentProperty, CellProp> a) {
		agents.add(a);
		a.addAgentListener(this);
		gridModelChanged(new GridModelEvent<AgentProperty, CellProp>(
				EventType.AGENT_ADDED, a));

	}

	public Agent<AgentProperty, CellProp> removeRandomAgent() {
		Agent<AgentProperty, CellProp> a = agents
				.remove((int) (Math.random() * agents.size()));
		a.removeAgentListener(this);
		gridModelChanged(new GridModelEvent<AgentProperty, CellProp>(
				EventType.AGENT_REMOVED, a));
		return a;
	}

	public void removeAgent(Agent<AgentProperty, CellProp> a) {
		agents.remove(a);
		a.removeAgentListener(this);
		gridModelChanged(new GridModelEvent<AgentProperty, CellProp>(
				EventType.AGENT_REMOVED, a));
	}

	public void addListener(GridModelListener<AgentProperty, CellProp> l) {
		listeners.add(GridModelListener.class, l);
	}

	public void removeListener(GridModelListener<AgentProperty, CellProp> l) {
		listeners.remove(GridModelListener.class, l);
	}

	public CellType getCellAt(short[] position) {
		return grid[position[0]][position[1]];
	}

	public CellType getCellAt(int x, int y) {
		return grid[x][y];
	}

	public CellType[][] getCells() {
		return grid;
	}

	public int getWidth() {
		return width;
	}

	public int getHeight() {
		return height;
	}

	public void addAttractor(Attractor<CellProp> a) {
		attractors.add(a);
		short[] pos = a.getCenter().getPosition();
		int radius = (int) a.getRadius();
		int xc = pos[0];
		int yc = pos[1];
		int xmin = xc - radius;
		int xmax = xc + radius;
		int ymin = yc - radius;
		int ymax = yc + radius;
		int nw = (int)Math.floor(xmin/(float)width)*width;
		int nh = (int)Math.floor(ymin/(float)height)*height;
		for (int x = xmin; x <= xmax; x++) {
			for (int y = ymin; y <= ymax; y++) {
				int X = ((x-nw)%width);
				int Y = ((y-nh)%height);
				CellType cell = this.grid[X][Y];
				cell.addAttractorEffect(x, y, a);
			}
		}
	}

	public static void main(String[] args) {
		System.out.println(Math.floor(1/2.));
	}
	
	public void removeAttractor(Attractor<CellProp> a) {
		attractors.remove(a);
		short[] pos = a.getCenter().getPosition();
		int radius = (int) a.getRadius();
		int xc = pos[0];
		int yc = pos[1];
		int xmin = xc - radius;
		int xmax = xc + radius;
		int ymin = yc - radius;
		int ymax = yc + radius;
		int nw = (int)Math.floor(xmin/(float)width)*width;
		int nh = (int)Math.floor(ymin/(float)height)*height;
		for (int x = xmin; x < xmax; x++) {
			for (int y = ymin; y < ymax; y++) {
				int X = ((x-nw)%width);
				int Y = ((y-nh)%height);
				CellType cell = this.grid[X][Y];
				cell.removeAttractorEffect(x, y, a);
			}
		}
	}
	
	public void addPhero(Attractor<CellProp> a) {
		attractors.add(a);
		short[] pos = a.getCenter().getPosition();
		int radius = (int) a.getRadius();
		int xc = pos[0];
		int yc = pos[1];
		int xmin = xc - radius;
		int xmax = xc + radius;
		int ymin = yc - radius;
		int ymax = yc + radius;
		int nw = (int)Math.floor(xmin/(float)width)*width;
		int nh = (int)Math.floor(ymin/(float)height)*height;
		for (int x = xmin; x <= xmax; x++) {
			for (int y = ymin; y <= ymax; y++) {
				int X = ((x-nw)%width);
				int Y = ((y-nh)%height);
				CellType cell = this.grid[X][Y];
				cell.addPheroEffect(x, y, a);
			}
		}
	}

//	static final private byte[] possibleDeplX = { 1, 1, 0, -1, -1, -1, 0, 1 };

//	static final private byte[] possibleDeplY = { 0, 1, 1, 1, 0, -1, -1, -1 };

	static final private byte[][] possibleDepl =
		{ { 1, 0 }, { 1, 1 }, { 0, 1 }, { -1, 1 }, { -1, 0 }, { -1, -1 }, { 0, -1 }, { 1, -1 } };
	static final private int pDNb = possibleDepl.length;
	

	public List<short[]> getNeighbours(short[] position) {
		List<short[]> result = new ArrayList<short[]>();
		short x = position[0];
		short y = position[1];
		for(short i=0;i<pDNb;i++) {
			byte[] depl = possibleDepl[i];
			short[] p = { (short)((x + width + depl[0]) % width),
					(short)((y + height + depl[1]) % height) };
			result.add(p);
		}
		return result;
	}
			
//		// Position n;
//		int x = position[0];
//		int y = position[1];
//		for (int i = 0; i < possibleDeplX.length; i++) {
//			int[] p = { (x + width + possibleDeplX[i]) % width,
//					(y + height + possibleDeplY[i]) % height };
//			result.add(p);
//		}
//		// for(int i=x-1;i<=x+1;i++) {
//		// int nx = (i+width)% width;
//		// for(int j=y-1;j<=y+1;j++) {
//		// int ny = (j+height)%height;
//		// n = new Position(nx,ny);
//		// if (!n.equals(p)) result.add(n);
//		// }
//		// }


	public int getNeighboursNb() {
		return pDNb;
	}

	final static double tcs = 1. / 360.;


	public short[] getNeighbour(short[] position, int direction) {
		short x = position[0];
		short y = position[1];
		byte[] depl = possibleDepl[direction];
		return new short[] { (short)((x + width + depl[0]) % width),
					(short)((y + height + depl[1]) % height) };
	}

	public int getDirection(short[] position, int direction, boolean usePhero){
		if(usePhero){
			double[] probas = getProbas(position, direction);
			return getNextPosition(probas, Math.random());
		}else if(freedom){
			return (int)(Math.random()*pDNb);
		}else{
			return (direction + (int)(Math.random()*(vision*2 + 1)) - vision + pDNb)%pDNb;
		}
	}

	private int vision = 2;

	private boolean freedom;
	
	public double[] getProbas(short[] pos, int direction) {
		double[] probas = new double[8];
		double sum = 0;
		double tmp;
		List<short[]> neighbours = getNeighbours(pos);
		int d;
		int i=0;
		if(freedom){
			CellType c;
			float ae;
			for (i=0;i<pDNb;i++) {
				c = getCellAt(neighbours.get(i));
				ae = c.getAttractorEffects();
				ae += c.getPheroEffects();
				tmp =  0.125 + ae;
				probas[i] = tmp;
				sum += tmp;
			}
			for (i=0;i<pDNb;i++) {
				probas[i] /= sum;
			}
		}else{
			d = direction;
			CellType c = getCellAt(neighbours.get(d));
			float ae = c.getAttractorEffects();
			ae += c.getPheroEffects();
			tmp =  0.125 + ae;
			probas[d] = tmp;
			sum += tmp;
			
			for (i=1;i<=vision;i++) {
				d = (direction+i)%pDNb;
				c = getCellAt(neighbours.get(d));
				ae = c.getAttractorEffects();
				ae += c.getPheroEffects();
				tmp =  0.125 + ae;
				probas[d] = tmp;
				sum += tmp;
				
				d = (direction+pDNb-i)%pDNb;
				c = getCellAt(neighbours.get(d));
				ae = c.getAttractorEffects();
				ae += c.getPheroEffects();
				tmp = 0.125 + ae;
				probas[d] = tmp;
				sum += tmp;
			}
			probas[direction] /= sum; 
			for (i=1;i<=vision;i++) {
				d = (direction+pDNb-i)%pDNb;
				probas[d] /= sum;
				d = (direction+i)%pDNb;
				probas[d] /= sum;
			}
		}
		return probas;
	}

	public int getNextPosition(double[] probas, double r) {
		boolean found = false;
		double sum = 0;
		int i = 0;
		while (!found && i < probas.length) {
			if (isIn(r, sum, sum + probas[i])) {
				found = true;
			}
			sum += probas[i];
			i++;
		}
		return i - 1;
	}
	
	public void cellChanged(CellEvent<CellProp> ce) {
		gridChanged(new GridModelEvent<AgentProperty, CellProp>(ce));
	}

	private void gridChanged(GridModelEvent<AgentProperty, CellProp> e) {
		for (GridModelListener<AgentProperty, CellProp> gml : listeners
				.getListeners(GridModelListener.class)) {
			gml.gridModelChanged(e);
		}
	}

	public void agentChanged(AgentEvent<AgentProperty, CellProp> event) {
		for (GridModelListener<AgentProperty, CellProp> gml : listeners
				.getListeners(GridModelListener.class)) {
			gml.gridModelChanged(new GridModelEvent<AgentProperty, CellProp>(
					event));
		}
	}

	public void gridModelChanged(GridModelEvent<AgentProperty, CellProp> event) {
		for (GridModelListener<AgentProperty, CellProp> gml : listeners
				.getListeners(GridModelListener.class)) {
			gml.gridModelChanged(event);
		}
	}

	/**
	 * @return the agents
	 */
	public Collection<Agent<AgentProperty, CellProp>> getAgents() {
		return agents;
	}


	private static boolean isIn(double a, double b, double c) {
		return b <= a && a <= c;
	}

	public void putPhero(short[] position, int i) {
		Attractor<CellProp> a = new Attractor<CellProp>((Cell<CellProp>) this.getCellAt(position),pheroForce,pheroRadius);
		addPhero(a);
	}

	public void evaporate() {
		for(int i=0;i<width;i++) {
			for(int j=0;j<height;j++) {
				grid[i][j].evaporate(evapRate);
			}
		}
	}

	public void setEvaporationRate(float evapRate){
		this.evapRate = evapRate;
	}
	
	public void clearAttractors(boolean attractor, boolean phero) {
		for(int i=0;i<width;i++) {
			for(int j=0;j<height;j++) {
				grid[i][j].clearAttractorEffect(attractor, phero);
			}
		}
	}

	public void setVision(Integer value) {
		this.vision = value;
		freedom = value > 3;
	}

	public void setPheroForce(double pheroForce2) {
		this.pheroForce = (float)pheroForce2;
	}

	public void setPheroRadius(float value) {
		this.pheroRadius = value;
	}
}
